--[[
	Project: NerfedButtons4Rift (NB4R)
	Version: @project-version@
	Author: NerfedWar (http://www.nerfedwar.net - nerfed.war@gmail.com)
	
	This program is free software: you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation, either version 3 of the License, or
	(at your option) any later version.
	
	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.
	
	You should have received a copy of the GNU General Public License
	along with this program.  If not, see <http://www.gnu.org/licenses/>.
]]--

-------------------------------------------------------------------------------
-- Start of globals >>

-- create the NB4R table
if not NB4R then NB4R = {} end

-- set to false to hide debug messages
NB4R.debug = false

-- caches ability name to id mapping
NB4R.abilityNameToIdMap = {}     

-- if the cooldown on an ability is longer than this then we ignore the ability
NB4R.GCD = 1.0001

-- lastCoordinates
NB4R.lastCoordinates = {}

-- is moving
NB4R.isMoving = false

-- time of last system update begin
NB4R.lastUpdateTime = 0

-- create holders for the 3 communication frames
NB4R.modFrame = nil
NB4R.keyFrame = nil
NB4R.togFrame = nil

NB4R.toggle = false

-- used to 'batch' calls to /NB4R into a sequence. All requests within this timescale
-- are trated as the same sequence
NB4R.sequenceWindow = 0.1

-- a cache for calls to costly Inspect.* functions
NB4R.inspectCache = {} 

-- the time we last had a successful ability match in a sequence
NB4R.lastSuccessTime = Inspect.Time.Real()     
NB4R.lastCmdTime = Inspect.Time.Real() 

-- holds a table of when abilities are used and their cooldown begins
NB4R.abilityLastUsed = {}

-- holds the id of the last hotekey request sent to ahk
NB4R.abilityIdForLastHotkeySent = nil

-- << End of globals
-------------------------------------------------------------------------------


-------------------------------------------------------------------------------
-- Start of utility functions >>

-- Simple split implementation
function NB4R.split(s, re)
	local i1 = 1
	local ls = {}
	local append = table.insert
	if not re then re = '%s+' end
	if re == '' then return { s } end
	while true do
		local i2, i3 = s:find(re, i1)
		if not i2 then
			local last = s:sub(i1)
			if last ~= '' then append(ls, last) end
			if #ls == 1 and ls[1] == '' then
				return {}
			else
				return ls
			end
		end
		append(ls, s:sub(i1, i2 - 1))
		i1 = i3 + 1
	end
end

-- returns the string in capitals without spaces and any single didget number tacked onto the end as a seperate return element
function NB4R.simplifyString(str)
	local stack = 1
	local ability = str
	if string.find(str, "%d$") then
	ability = string.sub(str, 1, -2) 
	stack = tonumber(string.sub(str, -1))
	end

	return string.upper(string.gsub( ability, "[^%w]", "" )), stack
end

-- << End of utility functions
-------------------------------------------------------------------------------


-- gets called when an ability is used and enters cooldown and is passed
-- a table containing the remaining cooldown on all abilities.
-- We compare this table with a copy from the previous time this function was called
-- to determine which ability has just (re)entered cooldown.
--
function NB4R.abilityBeginsCooldown(evt)

	local now=Inspect.Time.Real()

	-- if an ability was used by NB4R (rather than by a direc hotbutton)
	-- then we should have that ability id stored in NB4R.abilityIdForLastHotkeySent
	if NB4R.abilityIdForLastHotkeySent then
		NB4R.abilityLastUsed[NB4R.abilityIdForLastHotkeySent]=now
		NB4R.abilityIdForLastHotkeySent = nil
	end

end

-- Loop through the player list of abilities and store the name=id mappings
-- 
function NB4R.mapAbilities(abilityListing)
	for i, v in pairs(abilityListing) do
		local detail =  Inspect.Ability.New.Detail(i)
		if not NB4R.abilityNameToIdMap[detail.name] then
			NB4R.abilityNameToIdMap[detail.name] = detail.id	
		end
	end
end

-- Parse the /NB4R slash command
function NB4R.CMD(arg)

	-- get time now
	local now = Inspect.Time.Real()
	
	NB4R.lastCmdTime = now

	-- clean arg and extract pieces
	local clean = string.gsub(arg, "[ ]+", " ")
	clean = string.gsub(arg, "=[ ]", "=")
	clean = string.gsub(arg, "[ ]=", "=")
	clean = string.gsub(arg, ":[ ]", ":")
	clean = string.gsub(arg, "[ ]:", ":")
	clean = string.gsub(arg, "[ ]$", "$")
	local cmd = string.gsub(clean, "(.*)[ ](.*)[ ](.*)$", "%1|%2|%3")
	
	-- if we have a blank seperator line or a # comment line then return
	if cmd=="" or string.sub(cmd, 1, 1)=='#' then return end
	
	-- deal with dumps
	if cmd=="dump player" then
		print(" ")
		print("Player Buffs:")
		local pb = Inspect.Buff.List('player');
		for i, v in pairs(pb) do
			local bd = Inspect.Buff.Detail('player', i);
			print(" * "..bd.name)
		end
		print("Target Buffs:")
		local pb = Inspect.Buff.List('player');
		for i, v in pairs(pb) do
			local bd = Inspect.Buff.Detail('player', i);
			print(" * "..bd.name)
		end
		print("Player Abilities:")
		local pa = Inspect.Ability.New.List();
		for i, v in pairs(pa) do
			local ad = Inspect.Ability.New.Detail(i);
			print(" * "..ad.name)
		end
		return
	end

	-- get indices of all spaced
	local cmdTable = NB4R.split(cmd, "|")
	local cmdNum = table.getn(cmdTable)

	-- get vars and error if we dont have enough
	local ability = nil
	local conditions = nil
	local hotkey = nil
	if cmdNum == 3 then
		ability = cmdTable[1]
		conditions = cmdTable[2]
		hotkey = cmdTable[3]
	else
		print("Error: Invalid sequence format (/NB4R "..arg.."), please correct.")
		return
	end

	-- parse conditions
	local conditionTemp = NB4R.split(conditions, ":")
	conditions = {}
	for i, v in pairs(conditionTemp) do
		local conPair = NB4R.split(v, "=")
		conditions[conPair[1]]=conPair[2]
	end

	-- do we have a mapping for this ability
	if not NB4R.abilityNameToIdMap[ability] then
		-- update mappings
		NB4R.mapAbilities(Inspect.Ability.New.List())
	end
	
	-- set id or error if we still can't find the ability
	local id = nil
	if not NB4R.abilityNameToIdMap[ability] then
		print("Error: Invalid ability name ("..ability.."), please correct")
		return
	else
		id=NB4R.abilityNameToIdMap[ability]
	end

	-- we now have an id, lets get information about the ability
	local details = Inspect.Ability.New.Detail(id)

	-----------------------------------------------------------------------
	-- Start of condition checks >>
	
	-- perform Inspects
	-- player
	if not NB4R.inspectCache['player'] then
		NB4R.inspectCache['player'] = Inspect.Unit.Detail('player')
	end
	if not NB4R.inspectCache['target'] then
		NB4R.inspectCache['target'] = Inspect.Unit.Detail('player.target')
	end
	-- playerBuffs
	if not NB4R.inspectCache['playerBuffs'] then
		local buffList = Inspect.Buff.List('player')
		local buffInspectCache = {}
		for i,v in pairs(buffList) do
			local buffDetail = Inspect.Buff.Detail('player', i)
			local stack = 1
			if buffDetail.stack then stack = buffDetail.stack end
			local simpleString, _ = NB4R.simplifyString(buffDetail.name)
			buffInspectCache[simpleString]=tonumber(stack)
		end
		NB4R.inspectCache['playerBuffs'] = buffInspectCache
	end
	-- petBuffs
	if not NB4R.inspectCache['petBuffs'] then
		local buffList = Inspect.Buff.List('player.pet')
		local buffInspectCache = {}
		if buffList then
			for i,v in pairs(buffList) do
				local buffDetail = Inspect.Buff.Detail('player.pet', i)
				local stack = 1
				if buffDetail.stack then stack = buffDetail.stack end
				local simpleString, _ = NB4R.simplifyString(buffDetail.name)
				buffInspectCache[simpleString]=tonumber(stack)
			end
		end
		NB4R.inspectCache['petBuffs'] = buffInspectCache
	end
	-- targetBuffs
	if not NB4R.inspectCache['targetBuffs'] then
		local buffList = Inspect.Buff.List('player.target')
		if buffList then
			local buffInspectCache = {}
			for i,v in pairs(buffList) do
				local buffDetail = Inspect.Buff.Detail('player.target', i)
				local stack = 1
				if buffDetail.stack then stack = buffDetail.stack end
				local simpleString, _ = NB4R.simplifyString(buffDetail.name)
				buffInspectCache[simpleString]=tonumber(stack)
			end
			NB4R.inspectCache['targetBuffs'] = buffInspectCache
		end
	end
	
	-- range calc
	if not NB4R.inspectCache['playerTargetDistance'] then
		if NB4R.inspectCache['target'] then
			local pr = NB4R.inspectCache['player'].radius
			local tr = NB4R.inspectCache['target'].radius
			local dx = math.abs(NB4R.inspectCache['player'].coordX - NB4R.inspectCache['target'].coordX)-- - (pr+tr)
			local dy = math.abs(NB4R.inspectCache['player'].coordY - NB4R.inspectCache['target'].coordY)-- - (pr+tr)
			local dz = math.abs(NB4R.inspectCache['player'].coordZ - NB4R.inspectCache['target'].coordZ)-- - (pr+tr)
			NB4R.inspectCache['playerTargetDistance'] = math.sqrt((dx*dx) + (dy*dy) + (dz*dz))
		else
			NB4R.inspectCache['playerTargetDistance'] = 0
		end
	end
	
	-- reset GCD for rogues and spark mages
	-- TODO: add spark cooldown modification
	if NB4R.inspectCache['player'] then 
		if NB4R.inspectCache['player'].calling == "rogue" then
			NB4R.GCD = 1.0001
		 end
	end
	
	-- check range
	if details.outOfRange then
		NB4R.fail(now)
		return	
	end		
	
	-- check usable
	if details.unusable then
		NB4R.fail(now)
		return		
	end
		
	-- check cooldown (if the cooldown ends within the queue time we should send a hotkey request)
	if details.currentCooldownRemaining then
		if details.currentCooldownRemaining > NB4R.GCD then
			NB4R.fail(now)
			return
		end
	end

	-- range check
	if NB4R.inspectCache['playerTargetDistance'] then
		if conditions["RC"] then
			if (NB4R.inspectCache['playerTargetDistance'] <= tonumber(conditions["RC"])) then
			-- in range
			else
				NB4R.fail(now)
				return
			end
		end
	end
	
	-- check when the ability was last used. If it hasn't been used before initialise it last
	-- use time to zero (long time in the past)
	if not NB4R.abilityLastUsed[id] then  NB4R.abilityLastUsed[id]=0 end
	-- if we have a virtual cooldown then
	if conditions["VCD"] then
		if (NB4R.abilityLastUsed[id] + tonumber(conditions["VCD"])) - now > NB4R.GCD then
			NB4R.fail(now)
			return
		end
	end
	
	-- check player health
	if conditions["PH+"] then
		if (NB4R.inspectCache['player'].health/NB4R.inspectCache['player'].healthMax)*100 > tonumber(conditions["PH+"]) then
		-- PASS
	else
		-- FAIL
			NB4R.fail(now)
			return
		end
	end
	if conditions["PH-"] then
		if (NB4R.inspectCache['player'].health/NB4R.inspectCache['player'].healthMax)*100 < tonumber(conditions["PH-"]) then
		-- PASS
	else
		-- FAIL
			NB4R.fail(now)
			return
		end
	end
	
	-- check player mana
	if conditions["PM+"] then
		if (NB4R.inspectCache['player'].mana/NB4R.inspectCache['player'].manaMax)*100 > tonumber(conditions["PM+"]) then
		-- PASS
	else
		-- FAIL
			NB4R.fail(now)
			return
		end
	end
	if conditions["PM-"] then
		if (NB4R.inspectCache['player'].mana/NB4R.inspectCache['player'].manaMax)*100 < tonumber(conditions["PM-"]) then
		-- PASS
	else
		-- FAIL
			NB4R.fail(now)
			return
		end
	end	
	
	-- check player combo points
	if conditions["CP"] then
		if (NB4R.inspectCache['player'].combo >= tonumber(conditions["CP"])) then
		-- PASS
	else
		-- FAIL
			NB4R.fail(now)
			return
		end
	end
	
	-- check player charge
	if conditions["PC+"] then
		if (NB4R.inspectCache['player'].charge/NB4R.inspectCache['player'].chargeMax)*100 > tonumber(conditions["PC+"]) then
		-- PASS
	else
		-- FAIL
			NB4R.fail(now)
			return
		end
	end
	if conditions["PC-"] then
		if (NB4R.inspectCache['player'].charge/NB4R.inspectCache['player'].chargeMax)*100 < tonumber(conditions["PC-"]) then
		-- PASS
	else
		-- FAIL
			NB4R.fail(now)
			return
		end
	end
	
	-- check player energy
	if conditions["PE+"] then
		if (NB4R.inspectCache['player'].energy/NB4R.inspectCache['player'].energyMax)*100 > tonumber(conditions["PE+"]) then
		-- PASS
	else
		-- FAIL
			NB4R.fail(now)
			return
		end
	end
	if conditions["PE-"] then
		if (NB4R.inspectCache['player'].energy/NB4R.inspectCache['player'].energyMax)*100 < tonumber(conditions["PE-"]) then
		-- PASS
	else
		-- FAIL
			NB4R.fail(now)
			return
		end
	end

	-- check for a player buff
	if conditions["PB+"] then
		local simpleAbilityString, stacks = NB4R.simplifyString(conditions["PB+"])
		if (NB4R.inspectCache['playerBuffs'][simpleAbilityString] and NB4R.inspectCache['playerBuffs'][simpleAbilityString]>=stacks) then
		-- PASS
	else
		-- FAIL
			NB4R.fail(now)
			return
		end
	end
	if conditions["PB-"] then
		local simpleAbilityString, stacks = NB4R.simplifyString(conditions["PB-"])
		if (not NB4R.inspectCache['playerBuffs'][simpleAbilityString] or NB4R.inspectCache['playerBuffs'][simpleAbilityString]<stacks) then
		-- PASS
	else
		-- FAIL
			NB4R.fail(now)
			return
		end
	end
	
	-- check for a pet buff
	if conditions["PPB+"] then
		local simpleAbilityString, stacks = NB4R.simplifyString(conditions["PPB+"])
		if (NB4R.inspectCache['petBuffs'][simpleAbilityString]>=stacks) then
		-- PASS
	else
		-- FAIL
			NB4R.fail(now)
			return
		end
	end
	if conditions["PPB-"] then
		local simpleAbilityString, stacks = NB4R.simplifyString(conditions["PPB-"])
		if (not NB4R.inspectCache['petBuffs'][simpleAbilityString] or NB4R.inspectCache['petBuffs'][simpleAbilityString]<stacks) then
		-- PASS
	else
		-- FAIL
			NB4R.fail(now)
			return
		end
	end
	
	-- check for a target buff
	if conditions["TB+"] then
		if NB4R.inspectCache['targetBuffs'] then
			local simpleAbilityString, stacks = NB4R.simplifyString(conditions["TB+"])
			if (NB4R.inspectCache['targetBuffs'][simpleAbilityString] and NB4R.inspectCache['targetBuffs'][simpleAbilityString]>=stacks) then
			-- PASS
			else
			-- FAIL
				NB4R.fail(now)
				return
			end
		else
			NB4R.fail(now)
			return
		end
	end
	if conditions["TB-"] then
		if NB4R.inspectCache['targetBuffs'] then
			local simpleAbilityString, stacks = NB4R.simplifyString(conditions["TB-"])
			if (not NB4R.inspectCache['targetBuffs'][simpleAbilityString] or NB4R.inspectCache['targetBuffs'][simpleAbilityString]<stacks) then
			-- PASS
		else
			-- FAIL
				NB4R.fail(now)
				return
			end
		else
			NB4R.fail(now)
			return
		end
	end
	
	-- check for a player buff
	if conditions["INS"] then
		if details.castingTime then
		-- FAIL
			NB4R.fail(now)
			return
		end
	end
	
	-- check for moving
	if conditions["MOV-"] and NB4R.isMoving then
		NB4R.fail(now)
		return
	end
	
	-- check for not moving
	if conditions["MOV+"] and not NB4R.isMoving then
		NB4R.fail(now)
		return
	end
	
	-- check target health
	if conditions["TH+"] then
		if NB4R.inspectCache['target'] then
			if (NB4R.inspectCache['target'].health/NB4R.inspectCache['target'].healthMax)*100 > tonumber(conditions["TH+"]) then
			-- PASS
		else
			-- FAIL
				NB4R.fail(now)
				return
			end
		else
			NB4R.fail(now)
			return
		end
	end
	if conditions["TH-"] then
		if NB4R.inspectCache['target'] then
			if (NB4R.inspectCache['target'].health/NB4R.inspectCache['target'].healthMax)*100 < tonumber(conditions["TH-"]) then
			-- PASS
		else
			-- FAIL
				NB4R.fail(now)
				return
			end
		else
			NB4R.fail(now)
			return
		end
	end
	
	-- check target calling
	if conditions["TC"] then
		if NB4R.inspectCache['target'] then
			local actualTC = NB4R.inspectCache['target'].calling
			if conditions["TC"]=="C" and NB4R.inspectCache['target'].calling == "cleric" then
		-- PASS
				-- we have a cleric
			elseif conditions["TC"]=="M" and NB4R.inspectCache['target'].calling == "mage" then
		-- PASS
				-- we have a mage
			elseif conditions["TC"]=="R" and NB4R.inspectCache['target'].calling == "rogue" then
		-- PASS
				-- we have a rogue
			elseif conditions["TC"]=="W" and NB4R.inspectCache['target'].calling == "warrior" then
		-- PASS
				-- we have a warrior
			else
		-- FAIL
				NB4R.fail(now)
				return
			end
		else
			NB4R.fail(now)
			return
		end
	end

	-- << End of condition checks
	-----------------------------------------------------------------------
	
	-- send hotkey as we must have passed all checks
	-- if a hotkey has already been sent in this window then don't send any more
	-- clean out inspect cache
	if now > (NB4R.lastSuccessTime+NB4R.sequenceWindow) then 
		NB4R.lastSuccessTime = now
		NB4R.inspectCache = {}
		NB4R.abilityIdForLastHotkeySent = id
		NB4R.sendHotkey(hotkey)
		if NB4R.debug then print("All checks passed for ability: "..ability.."!") end
	end

end

-- create context and set to be topmost strata
local context = UI.CreateContext("Context")
context:SetStrata("topmost")

-- resets the inspectCache if all the abilities fail in a sequence
--
function NB4R.fail(now)
	if now > (NB4R.lastSuccessTime+NB4R.sequenceWindow) then 
	   if NB4R.debug then print("Ability in sequence failed its checks!") end
	   NB4R.inspectCache = {}
	end
end

-- 
function NB4R.update()
    -- we only want this running a maximum of once every 1/5 of a second
    local rightNow = Inspect.Time.Real()
    if (rightNow < NB4R.lastUpdateTime+0.2) then return end
    NB4R.lastUpdateTime = rightNow
    
    local p = Inspect.Unit.Detail('player')
    if p then
	NB4R.isMoving = true
	if 		NB4R.lastCoordinates['coordX'] == p.coordX
		and  	NB4R.lastCoordinates['coordY'] == p.coordY
		and 	NB4R.lastCoordinates['coordZ'] == p.coordZ then
		NB4R.isMoving = false
	end
	NB4R.lastCoordinates['coordX']=p.coordX
	NB4R.lastCoordinates['coordY']=p.coordY
	NB4R.lastCoordinates['coordZ']=p.coordZ
    end
end

-- create the 3 communication frames
function NB4R.init()

    NB4R.modFrame = UI.CreateFrame("Frame", "NB4RModFrame", context)
    NB4R.modFrame:SetPoint("TOPLEFT", UIParent, "TOPLEFT", 0, 0)
    NB4R.modFrame:SetHeight(10)
    NB4R.modFrame:SetWidth(10)
    NB4R.modFrame:SetLayer(9999)
    NB4R.modFrame:SetBackgroundColor(0.5, 0.5, 0.5, 1) 

    NB4R.keyFrame = UI.CreateFrame("Frame", "NB4RKeyFrame", context)
    NB4R.keyFrame:SetPoint("TOPLEFT", UIParent, "TOPLEFT", 10, 0)
    NB4R.keyFrame:SetHeight(10)
    NB4R.keyFrame:SetWidth(10) 
    NB4R.keyFrame:SetLayer(9999)
    NB4R.keyFrame:SetBackgroundColor(0.5, 0.5, 0.5, 1) 
    
    NB4R.togFrame = UI.CreateFrame("Frame", "NB4RTogFrame", context)
    NB4R.togFrame:SetPoint("TOPLEFT", UIParent, "TOPLEFT", 20, 0)
    NB4R.togFrame:SetHeight(10)
    NB4R.togFrame:SetWidth(10)   
    NB4R.togFrame:SetLayer(9999)
    NB4R.togFrame:SetBackgroundColor(0.5, 0.5, 0.5, 1) 

end

-- converts a string based hotkey into modifier/key parts and
-- updates the communication pixels
NB4R.sendHotkey = function(hotkey)

  local red   = 0
  local green = 0
  local blue  = 0  
  local key   = 0
  local tog   = 0
  
  -- pixel 1
  if hotkey:find("%^") then red = 1 end
  if hotkey:find("!") then green = 1 end
  if hotkey:find("%+") then blue = 1 end
  
  -- pixel 2
  key = hotkey:match('[-=%w]')  
  
  -- pixel 3    
  NB4R.toggle = not NB4R.toggle
  if NB4R.toggle then tog = 1 end
  
  -- set actual pixels
  NB4R.modFrame:SetBackgroundColor(red, green, blue, 1) 
  NB4R.keyFrame:SetBackgroundColor(string.byte(key, 1)/255, 0, 0, 1) 
  NB4R.togFrame:SetBackgroundColor(tog, 0, 0, 1) 
	
end

-- Register the /NB4R slash command
table.insert(Command.Slash.Register("nb"), {NB4R.CMD, "NB4R", "Slash Command"})
table.insert(Event.Ability.New.Cooldown.Begin, { NB4R.abilityBeginsCooldown, "NB4R", "Ability begins cooldown" })
table.insert(Event.System.Update.Begin, { NB4R.update, "NB4R", "System update begin" })
table.insert(Event.Addon.Startup.End, { NB4R.init, "NB4R", "Addon startup end" })
